/*
 * Copyright 2007 - 2025 Ralf Wisser.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.sf.jailer.ui;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.DefaultComboBoxModel;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

import net.sf.jailer.datamodel.Association;
import net.sf.jailer.datamodel.Cardinality;
import net.sf.jailer.datamodel.DataModel;
import net.sf.jailer.datamodel.Table;
import net.sf.jailer.util.SqlUtil;

/**
 * Lets user apply an action on a subset of a given {@link Association} set.
 *
 * @author Ralf Wisser
 */
public abstract class AssociationListUI extends javax.swing.JPanel {

	/**
	 * Types of {@link Association}.
	 */
	private enum AssociationType {
		CHILD("1Child", GREEN),
		ASSOC("2Association", Colors.Color_0_0_150),
		PARENT("3Parent", RED);

		public final String name;
		public final Color color;

		private AssociationType(String name, Color color) {
			this.name = name;
			this.color = color;
		}
	}

	/**
	 * Association model for this UI.
	 */
	public interface AssociationModel {

		/**
		 * The source table.
		 */
		String getSourceName();

		/**
		 * The destination table.
		 */
		String getDestinationName();

		/**
		 * The join-condition for joining source with destination table.
		 */
		String getJoinCondition();

		/**
		 * The cardinality.
		 */
		Cardinality getCardinality();

		AssociationType getType();

		/**
		 * <code>true</code> for reversed association.
		 */
		boolean isReversed();

		/**
		 * The name of the association.
		 */
		String getName();

		DataModel getDataModel();

	}

	/**
	 * Default implementation of an association model for this UI.
	 */
	public static class DefaultAssociationModel implements AssociationModel {

		protected final Association association;

		public DefaultAssociationModel(Association association) {
			this.association = association;
		}

		/**
		 * The source table.
		 */
		@Override
		public String getSourceName() {
			return association.getDataModel().getDisplayName(association.source);
		}

		/**
		 * The destination table.
		 */
		@Override
		public String getDestinationName() {
			return association.getDataModel().getDisplayName(association.destination);
		}

		/**
		 * The join-condition for joining source with destination table.
		 */
		@Override
		public String getJoinCondition() {
			return association.getUnrestrictedJoinCondition();
		}

		/**
		 * The cardinality.
		 */
		@Override
		public Cardinality getCardinality() {
			return association.getCardinality();
		}

		/**
		 * <code>true</code> for reversed association.
		 */
		@Override
		public boolean isReversed() {
			return association.reversed;
		}

		/**
		 * The name of the association.
		 */
		@Override
		public String getName() {
			return association.getName();
		}

		@Override
		public DataModel getDataModel() {
			return association.getDataModel();
		}

		@Override
		public AssociationType getType() {
			if (association.isInsertSourceBeforeDestination()) {
				return AssociationType.CHILD;
			}
			if (association.isInsertDestinationBeforeSource()) {
				return AssociationType.PARENT;
			}
			return AssociationType.ASSOC;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj instanceof DefaultAssociationModel) {
				return association.equals(((DefaultAssociationModel) obj).association);
			}
			return false;
		}

		@Override
		public int hashCode() {
			return association.hashCode();
		}

	}

	/**
	 * The model.
	 */
	private List<AssociationModel> model = new ArrayList<AssociationModel>();

	/**
	 * Selected associations.
	 */
	private Set<AssociationModel> selection = new HashSet<AssociationModel>();

	/**
	 * Hidden model associations.
	 */
	private Set<AssociationModel> hidden = new HashSet<AssociationModel>();

	/**
	 * All hidden associations.
	 */
	private Set<AssociationModel> hiddenAll = new HashSet<AssociationModel>();

	/**
	 * Keep order of associations according to the source-table?
	 */
	private final boolean stableSourceOrder;

	/** Creates new form AssociationListUI */
	public AssociationListUI(String actionButtonText, String actionButtonToolTip, boolean stableSourceOrder) {
		this.stableSourceOrder = stableSourceOrder;
		initComponents(); UIUtil.initComponents(this);
		doItButton.setText(actionButtonText);
		doItButton.setToolTipText(actionButtonToolTip);
		groupByComboBox.setModel(new DefaultComboBoxModel(GroupByDefinition.values()));
		groupByComboBox.setSelectedIndex(0);
		groupByComboBox.addItemListener(new ItemListener() {
			@Override
			public void itemStateChanged(ItemEvent e) {
				updateModel();
				revalidate();
			}
		});
		if (jScrollPane1.getVerticalScrollBar() != null) {
			jScrollPane1.getVerticalScrollBar().setUnitIncrement(16);
		}
		if (jScrollPane1.getHorizontalScrollBar() != null) {
			jScrollPane1.getHorizontalScrollBar().setUnitIncrement(16);
		}
	}

	private Double pixelPerTableNameChar = null;

	/**
	 * Sets the model.
	 *
	 * @param model the model
	 */
	public void setModel(Collection<AssociationModel> model) {
		this.model = new ArrayList<AssociationModel>(model);
		selection.retainAll(model);
		hidden.clear();
		hidden.addAll(hiddenAll);
		hidden.retainAll(model);
		if (pixelPerTableNameChar == null) {
			Iterator<AssociationModel> firstAssociationI = model.iterator();
			if (firstAssociationI.hasNext()) {
				AssociationModel firstAssociation = firstAssociationI.next();
				double size = 0.0;
				int l = 0;
				for (Table t: firstAssociation.getDataModel().getTables()) {
					String tName = firstAssociation.getDataModel().getDisplayName(t);
					size += new JLabel(tName).getMinimumSize().width;
					l += tName.length();
				}
				pixelPerTableNameChar = 8.0;
				if (l > 0) {
					pixelPerTableNameChar = size / l;
				}
				if (pixelPerTableNameChar < 5.0) {
					pixelPerTableNameChar = 5.0;
				}
			}
		}
		updateModel();
	}

	private static final Color GREEN = Colors.Color_0_130_0;
	private static final Color RED = Colors.Color_255_0_0;

	private static abstract class ColumnContentGetter {
		public abstract String getContent(AssociationModel association);
		public abstract String getDisplayName();
		public Color getFgColor(Collection<AssociationModel> group) {
			Color fg = null;
			for (AssociationModel associationModel: group) {
				Color c = associationModel.getType().color;
				if (fg == null || fg == c) {
					fg = c;
				} else {
					fg = null;
					break;
				}
			}
			return fg;
		}
	}

	private static class TypeGetter extends ColumnContentGetter {
		@Override
		public String getContent(AssociationModel association) {
			return association.getType().name;
		}
		@Override
		public String getDisplayName() {
			return "Type";
		}
	}

	private static class SourceGetter extends ColumnContentGetter {
		@Override
		public String getContent(AssociationModel association) {
			return association.getSourceName();
		}
		@Override
		public String getDisplayName() {
			return "From";
		}
		@Override
		public Color getFgColor(Collection<AssociationModel> group) {
			return null;
		}
	}

	private static class DestinationGetter extends ColumnContentGetter {
		@Override
		public String getContent(AssociationModel association) {
			return association.getDestinationName();
		}
		@Override
		public String getDisplayName() {
			return "To";
		}
	}

	private static class AssociationNameGetter extends ColumnContentGetter {
		@Override
		public String getContent(AssociationModel association) {
			return association.getName();
		}
		@Override
		public String getDisplayName() {
			return "Association Name";
		}
	}

	private class Node {
		public final Collection<AssociationModel> associations;
		public final List<Node> children = new ArrayList<AssociationListUI.Node>();
		public final String group;
		public final Color fgColor;

		public Node(String group, Collection<AssociationModel> associations, Color fgColor) {
			this.group = group;
			this.associations = associations;
			this.fgColor = fgColor;
		}

		public boolean isLinear() {
			return children.size() == 0 || children.size() == 1 && children.get(0).isLinear();
		}

		public boolean isSelected() {
			return selection.containsAll(associations);
		}

		public Collection<Node> getSubNodes() {
			Collection<Node> subNodes = new ArrayList<Node>();
			for (Node child: children) {
				subNodes.add(child);
				subNodes.addAll(child.getSubNodes());
			}
			return subNodes;
		}
	}

	private static ColumnContentGetter TYPE_CG = new TypeGetter();
	private static ColumnContentGetter SOURCE_CG = new SourceGetter();
	private static ColumnContentGetter DESTINATION_CG = new DestinationGetter();
	private static ColumnContentGetter ASSOCIATION_NAME_CG = new AssociationNameGetter();

	private enum GroupByDefinition {
		TSD("Type, From, To", new ColumnContentGetter[] { TYPE_CG, SOURCE_CG, DESTINATION_CG, ASSOCIATION_NAME_CG }),
		TDS("Type, To, From", new ColumnContentGetter[] { TYPE_CG, DESTINATION_CG, SOURCE_CG, ASSOCIATION_NAME_CG }),
		STD("From, Type, To", new ColumnContentGetter[] { SOURCE_CG, TYPE_CG, DESTINATION_CG, ASSOCIATION_NAME_CG }),
		DTS("To, Type, From", new ColumnContentGetter[] { DESTINATION_CG, TYPE_CG, SOURCE_CG, ASSOCIATION_NAME_CG }),
		SDT("From, To, Type", new ColumnContentGetter[] { SOURCE_CG, DESTINATION_CG, TYPE_CG, ASSOCIATION_NAME_CG }),
		DST("To, From, Type", new ColumnContentGetter[] { DESTINATION_CG, SOURCE_CG, TYPE_CG, ASSOCIATION_NAME_CG });

		public final ColumnContentGetter[] columnContentGetter;
		public final String displayName;

		private GroupByDefinition(String displayName, ColumnContentGetter[] columnContentGetter) {
			this.displayName = displayName;
			this.columnContentGetter = columnContentGetter;
		}

		@Override
		public String toString() {
			return displayName;
		}
	}

	private List<MouseListener> allMouseListener = new ArrayList<MouseListener>();
	private Map<Node, List<MouseListener>> mouseListenerPerNode = new HashMap<Node, List<MouseListener>>();
	private List<Node> roots;

	/**
	 * Hides selected associations.
	 */
	private void hideSelection() {
		hidden.addAll(selection);
		hiddenAll.addAll(selection);
		selection.clear();
		updateModel();
	}

	/**
	 * Unhides hidden associations.
	 */
	private void unhideHidden() {
		hidden.clear();
		hiddenAll.clear();
		updateModel();
	}

	/**
	 * Performes action on selected associations.
	 */
	protected abstract void applyAction(Collection<AssociationModel> selection);

	/**
	 * Updates the model UI.
	 */
	private void updateModel() {
		listPanel.removeAll();
		allMouseListener.clear();
		mouseListenerPerNode.clear();
		ColumnContentGetter[] columnContentGetter = ((GroupByDefinition) groupByComboBox.getSelectedItem()).columnContentGetter;
		Collection<AssociationModel> unhidden = new ArrayList<AssociationListUI.AssociationModel>(model);
		unhidden.removeAll(hidden);
		roots = createHierarchy(columnContentGetter, 0, unhidden);
		if (!unhidden.isEmpty()) {
			for (int x = 0; x <= columnContentGetter.length; ++x) {
				String text = x == 0? " " : columnContentGetter[x - 1].getDisplayName();
				JLabel title = new JLabel(text + "  ");
				title.setBackground(Colors.Color_255_255_255);
				title.setOpaque(true);
				title.setFont(bold);
				GridBagConstraints gridBagConstraints = new java.awt.GridBagConstraints();
				gridBagConstraints.gridx = x;
				gridBagConstraints.gridy = 0;
				gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
				listPanel.add(title, gridBagConstraints);
			}
		}

		int[] y = new int[] { 2 };
		updateUI(roots, 0, y, new boolean[] { false }, new HashMap<Integer, Integer>(), new Node[5]);
		JLabel l = new JLabel(" ");
		l.setBackground(Colors.Color_255_255_255);
		l.setOpaque(true);
		GridBagConstraints gridBagConstraints = new java.awt.GridBagConstraints();
		gridBagConstraints.gridwidth = 5;
		gridBagConstraints.gridx = 0;
		gridBagConstraints.gridy = y[0];
		gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
		gridBagConstraints.weighty = 1.0;
		gridBagConstraints.weightx = 1.0;
		listPanel.add(l, gridBagConstraints);

		infoLabel.setText(selection.size() + " selected, " + hidden.size() + " hidden ");
		hideButton.setEnabled(!selection.isEmpty());
		doItButton.setEnabled(!selection.isEmpty());
		unhideButton.setEnabled(!hidden.isEmpty());

		revalidate();
	}

	private void updateUI(List<Node> nodes, int level, int[] y, boolean[] groupStart, Map<Integer, Integer> minLevelPerY, Node[] lastRowContent) {
		boolean firstNode = true;
		Node pred = null;
		for (Node node: nodes) {
			GridBagConstraints gridBagConstraints;
			Color bgColor;
			if (y[0] % 2 == 0) {
				bgColor = Colors.Color_240_255_255;
			} else {
				bgColor = Colors.Color_255_255_255;
			}
			if (!minLevelPerY.containsKey(y[0])) {
				minLevelPerY.put(y[0], level);
			}
			if (!firstNode) {
				for (int x = 0; x < level; ++x) {
					String lastText = " ";
					Color lastFgColor = null;
					if (lastRowContent[x] != null) {
						lastText = lastRowContent[x].group;
						lastFgColor = lastRowContent[x].fgColor;
						if (lastFgColor != null) {
							lastFgColor = new Color(
									lastFgColor.getRed() + 2 * (255 - lastFgColor.getRed()) / 3,
									lastFgColor.getGreen() + 2 * (255 - lastFgColor.getGreen()) / 3,
									lastFgColor.getBlue() + 2 * (255 - lastFgColor.getBlue()) / 3);
						} else {
							lastFgColor = Colors.Color_192_192_192;
						}
					}
					JComponent l = createLabel(node, y[0], shorten(lastText), null, false, false, bgColor, lastFgColor, false, false);
					gridBagConstraints = new java.awt.GridBagConstraints();
					gridBagConstraints.gridx = x + 1;
					gridBagConstraints.gridy = y[0];
					gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
					listPanel.add(l, gridBagConstraints);
				}
			}
			groupStart[0] = groupStart[0] || (y[0] > 0 && (!node.isLinear() || pred != null && !pred.isLinear())); // level < minLevelPerY.get(y[0] - 1));
			AssociationModel a = node.associations.iterator().next();
			String joinCondition = a.getJoinCondition();
			if (a.isReversed()) {
				joinCondition = SqlUtil.reversRestrictionCondition(joinCondition);
			}
			JComponent l = createLabel(node, y[0], node.group, level >= 3? joinCondition : null, level < 3, groupStart[0], bgColor, level >= 3? Colors.Color_128_128_128 : node.fgColor, node.isSelected(), true);
			lastRowContent[level] = node;
			gridBagConstraints = new java.awt.GridBagConstraints();
			gridBagConstraints.gridx = level + 1;
			gridBagConstraints.gridy = y[0];
			gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
			if (level == 3) {
				gridBagConstraints.weightx = 1;
			}
			listPanel.add(l, gridBagConstraints);

			if (level >= 3) {
				++y[0];
				groupStart[0] = false;
				JPanel p = new JPanel();
				p.setLayout(new GridBagLayout());
				gridBagConstraints = new java.awt.GridBagConstraints();
				gridBagConstraints.gridx = 0;
				gridBagConstraints.gridy = y[0] - 1;
				gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
				listPanel.add(p, gridBagConstraints);
				gridBagConstraints = new GridBagConstraints();
				gridBagConstraints.gridx = 1;
				gridBagConstraints.gridy = 1;
				gridBagConstraints.weightx = 1;
				gridBagConstraints.weighty = 1;
				gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
				gridBagConstraints.insets = new Insets(2, 0, 2, 0);
				final JCheckBox checkbox = new JCheckBox("  ");
				p.add(checkbox, gridBagConstraints);
				p.setBackground(bgColor);
				checkbox.setBackground(bgColor);

				if (node.associations.size() != 1) {
					System.err.println("node.associations.size() != 1, " + node.associations.size());
				} else {
					final AssociationModel association = a;
					checkbox.setSelected(selection.contains(association));
					checkbox.addItemListener(new ItemListener() {
						@Override
						public void itemStateChanged(ItemEvent e) {
							if (checkbox.isSelected()) {
								selection.add(association);
								onSelect(association);
							} else {
								selection.remove(association);
							}
							updateModel();
						}
					});
				}
			} else {
				updateUI(node.children, level + 1, y, groupStart, minLevelPerY, lastRowContent);
			}
			firstNode = false;
			pred = node;
		}
	}

	private final Color BGCOLOR_OF_SELECTED_ROW = Colors.Color_0_255_255;
	private Font font = new JLabel("normal").getFont();
	private Font bold = font.deriveFont(font.getStyle() | Font.BOLD, font.getSize());
	private Font nonbold = font.deriveFont(font.getStyle() & ~Font.BOLD, font.getSize());

	private JComponent createLabel(final Node node, final int y, String text, String tooltip, boolean shorten, final boolean firstOfGroup, Color bgColor, Color fgColor, boolean selected, boolean addListener) {
		final JPanel panel = new JPanel() {
			@Override
			public void paint(Graphics graphics) {
				super.paint(graphics);
				if (firstOfGroup) {
					if (!(graphics instanceof Graphics2D))
						return;
					Graphics2D g2d = (Graphics2D) graphics;
					Color color = Colors.Color_128_128_128;
					g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
					g2d.setColor(color);
					g2d.setStroke(new BasicStroke());
					g2d.drawLine(0, 1, getSize().width - 1, 1);
				}
			}
			private static final long serialVersionUID = 5285941807747744395L;
		};
		if (selected) {
			bgColor = Colors.Color_220_225_255;
		}
		panel.setLayout(new GridBagLayout());
		final JLabel label = new JLabel();
		label.setOpaque(true);
		label.setFont(nonbold);
		if (fgColor != null) {
			label.setForeground(fgColor);
		}
		GridBagConstraints gridBagConstraints = new GridBagConstraints();
		gridBagConstraints.gridx = 1;
		gridBagConstraints.gridy = 1;
		gridBagConstraints.weightx = 1;
		gridBagConstraints.weighty = 1;
		gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
		gridBagConstraints.insets = new Insets(2, 0, 2, 0);
		panel.add(label, gridBagConstraints);
		if (text.trim().length() > 0) {
			label.setToolTipText(text);
		}
		if (tooltip != null) {
			label.setToolTipText(tooltip);
		}
		if (shorten) {
			text = shorten(text);
		}
		label.setText(text + "  ");
		label.setBackground(bgColor);
		panel.setBackground(bgColor);

		if (!addListener) {
			return panel;
		}
		MouseListener l;
		label.addMouseListener(l = new MouseListener() {
			Color bgColorL;
			Color bgColorP;
			@Override
			public void mouseReleased(MouseEvent e) {
			}
			@Override
			public void mousePressed(MouseEvent e) {
			}
			@Override
			public void mouseExited(MouseEvent e) {
				if (e != null) {
					for (Node subNode: node.getSubNodes()) {
						for (MouseListener l: mouseListenerPerNode.get(subNode)) {
							if (l != this) {
								l.mouseExited(null);
							}
						}
					}
				}
				if (bgColorL != null) {
					label.setBackground(bgColorL);
				}
				if (bgColorP != null) {
					panel.setBackground(bgColorP);
				}
			}
			@Override
			public void mouseEntered(MouseEvent e) {
				if (e != null) {
					for (MouseListener l: allMouseListener) {
						if (l != this) {
							l.mouseExited(null);
						}
					}
					for (Node subNode: node.getSubNodes()) {
						for (MouseListener l: mouseListenerPerNode.get(subNode)) {
							if (l != this) {
								l.mouseEntered(null);
							}
						}
					}
				}
				if (bgColorL == null) {
					bgColorL = label.getBackground();
				}
				if (bgColorP == null) {
					bgColorP = panel.getBackground();
				}
				panel.setBackground(BGCOLOR_OF_SELECTED_ROW);
				label.setBackground(BGCOLOR_OF_SELECTED_ROW);
			}
			@Override
			public void mouseClicked(MouseEvent e) {
				if (e.getButton() == MouseEvent.BUTTON1) {
					if (node.isSelected()) {
						selection.removeAll(node.associations);
					} else {
						selection.addAll(node.associations);
						Node leaf = node;
						while (leaf.children.size() > 0) {
							leaf = leaf.children.get(0);
						}
						if (leaf.associations.size() > 0) {
							onSelect(leaf.associations.iterator().next());
						}
					}
					updateModel();
				}
			}
		});
		allMouseListener.add(l);
		if (mouseListenerPerNode.get(node) == null) {
			mouseListenerPerNode.put(node, new ArrayList<MouseListener>());
		}
		mouseListenerPerNode.get(node).add(l);
		return panel;
	}

	protected void onSelect(AssociationModel association) {
	}

	private Map<String, String> shortForms = new HashMap<String, String>();

	private String shorten(String text) {
		if (shortForms.containsKey(text)) {
			return shortForms.get(text);
		}
		Pattern p = Pattern.compile("^(.*) (\\([0-9]*\\))$");
		Matcher matcher = p.matcher(text);
		String pre = text;
		String suf = "";
		if (matcher.matches()) {
			pre = matcher.group(1);
			suf = matcher.group(2);
		}

		String shortText = text;
		int maxWidth = (int) ((pixelPerTableNameChar == null? 8 : pixelPerTableNameChar) * 30.0);

		for (int i = pre.length() - 2; i > 3; --i) {
			if (new JLabel(shortText).getMinimumSize().width <= maxWidth) {
				break;
			}
			shortText = pre.substring(0, i) + "..." + suf;
		}

		shortForms.put(text, shortText);
		return shortText;
	}

	/**
	 * Create hierarchy of associations.
	 *
	 * @param columnContentGetter the {@link ColumnContentGetter}
	 * @param i index of current {@link ColumnContentGetter}
	 * @return list of nodes
	 */
	private List<Node> createHierarchy(ColumnContentGetter[] columnContentGetter, int i, Collection<AssociationModel> associations) {
		SortedMap<String, Collection<AssociationModel>> groups = new TreeMap<String, Collection<AssociationModel>>();
		List<String> groupKeys = new ArrayList<String>();
		for (AssociationModel association: associations) {
			String key = columnContentGetter[i].getContent(association);
			if (!groups.containsKey(key)) {
				groups.put(key, new ArrayList<AssociationModel>());
				groupKeys.add(key);
			}
			groups.get(key).add(association);
		}
		List<Node> result = new ArrayList<Node>();
		Iterable<String> gk = groups.keySet();
		if (stableSourceOrder && (columnContentGetter[i] instanceof SourceGetter)) {
			gk = groupKeys;
		}
		for (String groupKey: gk) {
			Collection<AssociationModel> group = groups.get(groupKey);
			String gName = groupKey;
			if (columnContentGetter[i] instanceof TypeGetter) {
				gName = gName.substring(1);
			}
			Node node = new Node(gName, group, columnContentGetter[i].getFgColor(group));
			result.add(node);
			if (i + 1 < columnContentGetter.length) {
				node.children.addAll(createHierarchy(columnContentGetter, i + 1, group));
			}
		}
		return result;
	}

	/** This method is called from within the constructor to
	 * initialize the form.
	 * WARNING: Do NOT modify this code. The content of this method is
	 * always regenerated by the Form Editor.
	 */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        jPanel1 = new javax.swing.JPanel();
        jScrollPane1 = new javax.swing.JScrollPane();
        listPanel = new javax.swing.JPanel();
        infoLabel = new javax.swing.JLabel();
        jPanel2 = new javax.swing.JPanel();
        jLabel1 = new javax.swing.JLabel();
        groupByComboBox = new javax.swing.JComboBox();
        jPanel4 = new javax.swing.JPanel();
        hideButton = new javax.swing.JButton();
        unhideButton = new javax.swing.JButton();
        doItButton = new javax.swing.JButton();

        setLayout(new java.awt.BorderLayout());

        jPanel1.setLayout(new java.awt.GridBagLayout());

        listPanel.setLayout(new java.awt.GridBagLayout());
        jScrollPane1.setViewportView(listPanel);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        jPanel1.add(jScrollPane1, gridBagConstraints);

        infoLabel.setText(" 12 selected, 0 hidden");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.insets = new java.awt.Insets(0, 4, 0, 8);
        jPanel1.add(infoLabel, gridBagConstraints);

        jPanel2.setLayout(new java.awt.GridBagLayout());

        jLabel1.setText(" Sorted by ");
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        jPanel2.add(jLabel1, gridBagConstraints);

        groupByComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" }));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        jPanel2.add(groupByComboBox, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 2;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.gridheight = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST;
        gridBagConstraints.weightx = 1.0;
        jPanel1.add(jPanel2, gridBagConstraints);

        jPanel4.setLayout(new java.awt.GridBagLayout());

        hideButton.setText("Hide");
        hideButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                hideButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 1;
        jPanel4.add(hideButton, gridBagConstraints);

        unhideButton.setText("Unhide");
        unhideButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                unhideButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 2;
        gridBagConstraints.gridy = 1;
        jPanel4.add(unhideButton, gridBagConstraints);

        doItButton.setText("doIt");
        doItButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                doItButtonActionPerformed(evt);
            }
        });
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 3;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
        gridBagConstraints.weightx = 1.0;
        jPanel4.add(doItButton, gridBagConstraints);

        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 3;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        jPanel1.add(jPanel4, gridBagConstraints);

        add(jPanel1, java.awt.BorderLayout.CENTER);
    }// </editor-fold>//GEN-END:initComponents

	private void hideButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_hideButtonActionPerformed
		hideSelection();
	}//GEN-LAST:event_hideButtonActionPerformed

	private void unhideButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_unhideButtonActionPerformed
		unhideHidden();
	}//GEN-LAST:event_unhideButtonActionPerformed

	private void doItButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_doItButtonActionPerformed
		if (selection.size() < 12 || JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(this, "Do you really want to resolve " + selection.size() + " associations?", "Resolve many associations", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE)) {
			applyAction(new ArrayList<AssociationListUI.AssociationModel>(selection));
		}
	}//GEN-LAST:event_doItButtonActionPerformed


    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton doItButton;
    private javax.swing.JComboBox groupByComboBox;
    private javax.swing.JButton hideButton;
    private javax.swing.JLabel infoLabel;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JPanel jPanel4;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JPanel listPanel;
    private javax.swing.JButton unhideButton;
    // End of variables declaration//GEN-END:variables

	private static final long serialVersionUID = -5302225732569622137L;

}
